package com.pucrs.ads.simulator;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Random;

import com.pucrs.ads.main.Main;

public class Simulator {

	private static Simulator instance;
	private Random random;
	private double lastRandomArrivingTime;
	private double lastRandomAttendanceTime;
	private double time = 0;
	private int inAttendance = 0;
	private ArrayList<Double> states;
	private ArrayList<Event> events;

	/**
	 * @author gutoback@gmail.com
	 * @return
	 * Metodo responsavel por inicializar  o simulador
	 */
	public static Simulator getInstance(){
		if(instance == null){ 
			instance = new Simulator();
			instance.setRandom(new Random());
			instance.setStates(new ArrayList<Double>(Main.MAX_CLIENT_QTD +1));
			instance.setEvents(new ArrayList<Event>());
		}
		return instance;
	}

	/**
	 * Metodo responsavel por realizar a simulacao, criar o evento final e processar os eventos
	 */
	public void simulate(){
		System.out.println("Iniciando Simulacao " + Calendar.getInstance().getTime());
		for(int i =0; i < Main.MAX_CLIENT_QTD + 1; i++){
			states.add(0.0);
		}
		scheduleLastEvent();
		scheduleArrivingEvent();
		while (time <= Main.FINAL_TIME){
			Event event = events.remove(0);
			if(event.getType().equals(EventType.END)){
				break;
			}
			processEvent(event);
		}
		processEnd();
	}

	/**
	 * Criacao do evento final conforme tempo passado na inicializacao do simulador
	 */
	public void scheduleLastEvent(){
		events.add(new Event(EventType.END,Main.FINAL_TIME,0.0));
	}

	/**
	 * Metodo que apresenta as probabilidades finais da simulacao, por estado
	 */
	public void processEnd(){
		for(int i =0; i < states.size(); i++){
			System.out.println("Probabilidade estado: " + i + " = " + Main.DF.format((100 * states.get(i)) / time) + "%");
		}
		System.out.println("Finalizando Simulacao " + Calendar.getInstance().getTime());
	}

	/**
	 * Metodo que processa os eventos de chegada, conforme algoritmo visto em aula
	 * @param event
	 */
	public void processArrival(Event event){
		if(inAttendance < Main.MAX_CLIENT_QTD){
			inAttendance++;
			if(inAttendance == 1){
				scheduleLeavingEvent();
			}
		}
		scheduleArrivingEvent();
	}
	
	/**
	 * Metodo que processa os eventos de saida, conforme algoritmo visto em aula
	 * @param event
	 */
	public void processDeparture(Event event){
		inAttendance--;
		if(inAttendance > 0){
			scheduleLeavingEvent();
		}
	}
	
	/**
	 * Metodo que agenda um evento de chegada
	 */
	public void scheduleArrivingEvent(){
		events.add(new Event(EventType.ARRIVE,(generateRandomClientArrivingTime() + time), lastRandomArrivingTime));
		Collections.sort(events);
	}
	
	/**
	 * Metodo que agenda um evento de saida
	 */
	public void scheduleLeavingEvent(){
		events.add(new Event(EventType.LEAVE,(generateRandomClientAttendanceTime() + time), lastRandomAttendanceTime));
		Collections.sort(events);
	}
	
	public void debugStates(){
		System.out.println("Em atendimento: " + inAttendance);
		System.out.println("Tempo: " + Main.DF.format(time));
		for(int i =0; i < states.size(); i++){
			System.out.println("Estado: " + i + " Tempo= " + Main.DF.format(states.get(i)));
		}
	}
	
	/**
	 * Metodo que processa os eventos, e conforme o tipo chama o processamento correto, e seta o tempo percorrido em cada estado
	 * @param event
	 */
	public void processEvent(Event event){
		if(Main.DEBUG == 1){
			System.out.println(event.toString());
			debugStates();
			System.out.println("-------------------------------------------------");
		}

		states.set(inAttendance, (states.get(inAttendance) + (event.getWhen() - time)));
		time = event.getWhen();
		if(event.getType().equals(EventType.ARRIVE)){
			processArrival(event);
		}else if(event.getType().equals(EventType.LEAVE)){
			processDeparture(event);
		}
	}

	/**
	 * Metodo que gera um tempo aleatorio de chegada entre os tempos minimos e maximos parametrizados na inicializacao da simulacao
	 * @return
	 */
	public double generateRandomClientArrivingTime(){
		lastRandomArrivingTime = (random.nextDouble() *(Main.MAX_CLIENT_ARRIVAL_TIME - Main.MIN_CLIENT_ARRIVAL_TIME)) + Main.MIN_CLIENT_ARRIVAL_TIME;
		return lastRandomArrivingTime;
	}

	/**
	 * Metodo que gera um tempo aleatorio de atendimento entre os tempos minimos e maximos parametrizados na inicializacao da simulacao
	 * @return
	 */
	public double generateRandomClientAttendanceTime(){
		lastRandomAttendanceTime = (random.nextDouble() *(Main.MAX_CLIENT_ATTENDANCE_TIME - Main.MIN_CLIENT_ATTENDANCE_TIME)) + Main.MIN_CLIENT_ATTENDANCE_TIME;
		return lastRandomAttendanceTime;
	}

	public Random getRandom() {
		return random;
	}

	public void setRandom(Random random) {
		this.random = random;
	}

	public double getLastRandomArrivingTime() {
		return lastRandomArrivingTime;
	}

	public void setLastRandomArrivingTime(double lastRandomArrivingTime) {
		this.lastRandomArrivingTime = lastRandomArrivingTime;
	}

	public double getLastRandomAttendanceTime() {
		return lastRandomAttendanceTime;
	}

	public void setLastRandomAttendanceTime(double lastRandomAttendanceTime) {
		this.lastRandomAttendanceTime = lastRandomAttendanceTime;
	}

	public ArrayList<Double> getStates() {
		return states;
	}

	public void setStates(ArrayList<Double> estados) {
		this.states = estados;
	}

	public ArrayList<Event> getEvents() {
		return events;
	}

	public void setEvents(ArrayList<Event> events) {
		this.events = events;
	}

}
